package host.discoveryProtocol;

import homeMadeExceptions.NotYetImplementedException;
import host.DiscoveryNetworkSocket;
import host.routingProtocol.StandardRoutingProtocol;

import java.util.HashMap;
import java.util.Vector;

import datatypes.BluetoothDeviceAddress;
import datatypes.Host;
import datatypes.Message;
/**
 * German:
 * 
 * For get actually Information and not outdatet ones, the registry sends in auitemporal Spaces
 * Ping Messages for an known Entry in Registry. If no Host answers with pong, device an service information
 * is outdatet, an will be deletet in next Time the delete will be processed
 * 
 * GENERAL MESSAGE CONTENT DESIGN
 * 0--> Message_Type
 * 1--> Vector<Object> ParameterList
 * 2--> in Response: Vector<Object>
 * 3,..*--> further informations / ResultSets
 * 
 * @author Alex
 *
 */
public class DistributedKnowledgeDiscovery50 extends DiscoveryNetworkSocket {

	
	///////////////////////////////////////////
	
	private enum message_type {
		PUBLISH, UPDATE, DELETE, PING, GET_SERVICES, GET_SERVICEPROVIDER, PONG, RESP_GET_SERVICEPROVIDER, RESP_GET_SERVICES
	}

	
	private HashMap <String, BluetoothDeviceAddress> hostsAddedMeAsNeighbour = new HashMap<String, BluetoothDeviceAddress>();
	private HashMap <String, BluetoothDeviceAddress> neighbourhood = new HashMap<String, BluetoothDeviceAddress>();
	private HashMap <String, Integer> pingIntervall = new HashMap<String, Integer>();
	private HashMap <String, BluetoothDeviceAddress> connections = new HashMap <String, BluetoothDeviceAddress>();
	private ServiceTable serviceTable = new ServiceTable();
	private double DitributionFactor = 0.5;
	
	private int maxPingIntervall = 10;
	//the host this socket belongs to
	private Host host = null;

	private DistributedKnowledgeDiscovery50(Host host) {
		super(host);
		this.host = host;
	}
	
	public static DistributedKnowledgeDiscovery50 getSocket(Host host){
		return new DistributedKnowledgeDiscovery50(host);
	}

	@Override
	public void enableDiscoveryMechanism() {
		super.enableDiscoveryMechanism();
	}

	@Override
	public void disableDiscoveryMechanism() {
		super.disableDiscoveryMechanism();
	}

	/**
	 * Check out the services of a special device
	 * 
	 * content organisation
	 * 0. ActionType
	 * 1. sender
	 * 2. target
	 * asynchonous --> void (normaly returns a Vector with the Services of the Remote host as String)
	 */
	public void askHostForServices(BluetoothDeviceAddress target) {
		if (this.serviceTable.getServicesForHost(target) != null){
			this.processResultAskForService(target, this.serviceTable.getServicesForHost(target));
		} else {
			this.askHostForServicesDirectly(target);
		}	
	}
	
	private void askHostForServicesDirectly(BluetoothDeviceAddress target) {
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.GET_SERVICES);
		Vector<Object> parameterList = new Vector<Object>();
		parameterList.add(target);
		messageContent.add(parameterList);
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, target);	
	}

	/**
	 * asynchonous --> void (normaly returns a Vector with the Provider of the Remote host as BluetoothDeviceAddress)
	 */
	public void findSpecificService(String serviceDescription) {
		if (this.serviceTable.getHostsForService(serviceDescription) != null){
			Vector<BluetoothDeviceAddress> result = this.serviceTable.getHostsForService(serviceDescription);
			for (BluetoothDeviceAddress bdaddr : result){
				this.processResultFindService(bdaddr, serviceDescription);				
			}
		} else {
			this.askNeighbourHoodForService(serviceDescription);
		}	
	}

	private void askNeighbourHoodForService(String serviceDescription) {
		/**
		 * 0--> Message_Type 
		 * 1--> Vector<Object> ParameterList 
		 * 2--> in Response:
		 * Vector<Object> 3,..*--> further informations / ResultSets
		 */
		for (BluetoothDeviceAddress neighbour : this.neighbourhood.values()){
			//prepare for NetworkSend
			Vector<Object> messageContent = new Vector<Object>();
			messageContent.add(message_type.GET_SERVICEPROVIDER);
			Vector<Object> parameterList = new Vector<Object>();
			parameterList.add(serviceDescription);
			messageContent.add(parameterList);
			Message msg = new Message (messageContent, true);
			//publish
			this.invokeSendMessage(msg, neighbour);				
		}

	}

	/**
	 * asynchonous --> void (normally returns a Vector with the available Services as Strings)
	 */
	public void getAllAvailableServices() {
		this.processResultGetAllServices(this.host.getBd_Addr(), this.serviceTable.getAllAvailableServices());
	}

	public void publishLocalServices(String newService) {
		this.host.getServices().add(newService);
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.PUBLISH);
		Vector<Object> parameterList = new Vector<Object>();
		parameterList.add(newService);
		messageContent.add(parameterList);
		
		for (BluetoothDeviceAddress dev : this.hostsAddedMeAsNeighbour.values()){
			Message msg = new Message (messageContent, true);
			//publish
			this.invokeSendMessage(msg, dev);				
		}
		
	}

	public void updateLocalService (String oldService, String newService){
		for (int i = 0; i < this.host.getServices().size() ; i++){
			if (this.host.getServices().get(i).equals(oldService)){
				this.host.getServices().remove(i);
				this.host.getServices().add(newService);		
			}
		}
		
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.UPDATE);
		Vector<Object> parameterList = new Vector<Object>();
		parameterList.add(oldService);
		parameterList.add(newService);
		messageContent.add(parameterList);
		
		for (BluetoothDeviceAddress dev : this.hostsAddedMeAsNeighbour.values()){
			Message msg = new Message (messageContent, true);
			//publish
			this.invokeSendMessage(msg, dev);				
		}
	}
	
	public void deleteLocalService (String service){
		for (int i = 0; i < this.host.getServices().size() ; i++){
			if (this.host.getServices().get(i).equals(service)){
				this.host.getServices().remove(i);
			}
		}
		
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.DELETE);
		Vector<Object> parameterList = new Vector<Object>();
		Vector<String> tmp = new Vector<String>();
		tmp.add(service);
		parameterList.add(tmp);
		messageContent.add(parameterList);
		
		for (BluetoothDeviceAddress dev : this.hostsAddedMeAsNeighbour.values()){
			Message msg = new Message (messageContent, true);
			//publish
			this.invokeSendMessage(msg, dev);				
		}
	}

	public void pingNeighbour (){
		
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.PING);
		for (BluetoothDeviceAddress dev : this.neighbourhood.values()){
			if (pingIntervall.get(dev.getAddress()) > 2 * maxPingIntervall){
				neighbourhood.remove(dev.getAddress());
			} else {
				if (pingIntervall.get(dev.getAddress()) == maxPingIntervall){
					Message msg = new Message (messageContent, true);
					//publish
					this.invokeSendMessage(msg, dev);									
				}
				Integer intervall = pingIntervall.get(dev.getAddress());
				intervall ++;
			}
		}
	}

	
	/**
	 * second Parameter is the channel , not the target... the target is addressed as Group by the channel
	 */
	@Override
	protected void invokeSendMessage(Message msg, BluetoothDeviceAddress target) {
		super.invokeSendMessage(msg, target);
	}

	///////////////////////////////////////////
	
	
	@Override
	public void notificationAPI() {
		this.pingNeighbour();
		Vector<BluetoothDeviceAddress> allAvailableDevices = ((StandardRoutingProtocol)host.getRoutingSocket()).getAllAvailableDevices();
		while (this.neighbourhood.size() < allAvailableDevices.size() * this.DitributionFactor )
		{
			addRandomHostToNeigbours();
		}
		System.out.println("Host: " + this.host.getBd_Addr().getAddress() + " Number Neighbours: " + this.neighbourhood.size() + " Number Available Host: " + ((StandardRoutingProtocol)this.host.getRoutingSocket()).getAllAvailableDevices().size());
	}

	@Override
	public void onConnectionEstablished(BluetoothDeviceAddress remoteHost) {
		this.connections.put(remoteHost.getAddress(), remoteHost);
		Vector<BluetoothDeviceAddress> allAvailableDevices = ((StandardRoutingProtocol)host.getRoutingSocket()).getAllAvailableDevices();
		while (this.neighbourhood.size() < allAvailableDevices.size() * this.DitributionFactor )
		{
			addRandomHostToNeigbours();
		}
	}
	
	private void addRandomHostToNeigbours() {
		Vector<BluetoothDeviceAddress> allAvailableDevices = ((StandardRoutingProtocol)host.getRoutingSocket()).getAllAvailableDevices();
		boolean newDeviceAdded = false;
		while (!newDeviceAdded){
			int randomNumber = (int) Math.round(Math.random() * allAvailableDevices.size());
			if (randomNumber < allAvailableDevices.size()){
				BluetoothDeviceAddress randomHost = allAvailableDevices.get(randomNumber);
				if (this.neighbourhood.get(randomHost.getAddress()) == null){
					this.neighbourhood.put(randomHost.getAddress(), randomHost);
					this.pingIntervall.put(randomHost.getAddress(), 0);
					newDeviceAdded = true;
					this.sendGetServicesToNewDevice(randomHost);
				}				
			}
		}
	}

	private void sendGetServicesToNewDevice(BluetoothDeviceAddress target) {
		/**
		 * 0--> Message_Type 
		 * 1--> Vector<Object> ParameterList 
		 * 2--> in Response:
		 * Vector<Object> 3,..*--> further informations / ResultSets
		 */
		
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.GET_SERVICES);
		Vector<Object> parameterList = new Vector<Object>();
		parameterList.add(target);
		messageContent.add(parameterList);
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, target);	

	}

	@Override
	public void onConnectionLost(BluetoothDeviceAddress remoteHost) {
		if (this.neighbourhood.get(remoteHost.getAddress()) != null){
			this.neighbourhood.remove(remoteHost.getAddress());
			while (this.neighbourhood.size() < ((StandardRoutingProtocol)host.getRoutingSocket()).getAllAvailableDevices().size() * this.DitributionFactor )
			{
				addRandomHostToNeigbours();
			}			
		} 
	}

	@Override
	public void onMessageACK(Message msg, BluetoothDeviceAddress target) {
		//fine its great but unintresting
		System.out.println("Yippie an Message Ack");
	}

	@Override
	public void onMessageLost(Message msg, BluetoothDeviceAddress target) throws NotYetImplementedException {
		if (this.neighbourhood.get(target.getAddress()) != null){
			this.neighbourhood.remove(target.getAddress());
			if (this.neighbourhood.size() < ((StandardRoutingProtocol)host.getRoutingSocket()).getAllAvailableDevices().size() * this.DitributionFactor){
				addRandomHostToNeigbours();
			}
		}
	}

	@SuppressWarnings("unchecked")
	public void receiveMessage(Message msg, BluetoothDeviceAddress sender) {
		//Only handly discovery messages
		if (msg.isDiscoveryMessage()){
			//temporary extract static content
			Vector<Object> content = ((Vector<Object>)msg.getContent());
			//--> Content consists out of max. 3 Parts
			//-->1->> Type of Message
			//-->2->> ParameterList for RPC
			//-->3->> Answerlist of executed RPC
			message_type type = (message_type) content.get(0);
			switch (type){
			case PING: //DONE
				//prepare for NetworkSend
				Vector<Object> messageContent = new Vector<Object>();
				messageContent.add(message_type.PONG);
				Message message = new Message (messageContent, true);
				//publish
				this.invokeSendMessage(message, sender);
				break;
			case PONG:
				this.pingIntervall.put(sender.getAddress(), 0);
				break;
			case DELETE:
				Vector<Object> parameter_del = (Vector<Object>) content.get(1);

				this.serviceTable.removeServices(sender, (Vector<String>)parameter_del.get(0));
				break;
			case GET_SERVICEPROVIDER:
				Vector<Object> parameter_getsp = (Vector<Object>) content.get(1);
				String searchedService = (String) parameter_getsp.get(0);
				Vector<BluetoothDeviceAddress> result = this.serviceTable.getHostsForService(searchedService);
				this.respGetServiceProvider(sender, searchedService, result);
			break;
			case RESP_GET_SERVICEPROVIDER:
				Vector<Object> parameter_getrsp = (Vector<Object>) content.get(1);
				String searchedServ = (String) parameter_getrsp.get(0);
				Vector<String> tmptmp = new Vector<String>();
				tmptmp.add(searchedServ);
				Vector<BluetoothDeviceAddress> providerList = (Vector<BluetoothDeviceAddress>) parameter_getrsp.get(1);
				if (providerList != null){
					for (BluetoothDeviceAddress provider : providerList){
						this.serviceTable.addDevice(provider, tmptmp);
						this.processResultFindService(provider, searchedServ);
					}	
				}
				break;
			case GET_SERVICES:
				Vector<Object> parameter_gets = (Vector<Object>) content.get(1);
				BluetoothDeviceAddress target = (BluetoothDeviceAddress) parameter_gets.get(0);
				Vector<String> result_gets = this.serviceTable.getServicesForHost(target);
				this.respGetServices(sender, result_gets);
				break;			
			case RESP_GET_SERVICES:
				Vector<Object> parameter_getrs = (Vector<Object>) content.get(1);
				Vector<String> searchedServices = (Vector<String>) parameter_getrs.get(0);
				this.serviceTable.addDevice(sender, searchedServices);
				this.processResultAskForService(sender, searchedServices);
				break;
			case PUBLISH:
				Vector<Object> parameter_pub = (Vector<Object>) content.get(1);
				Vector<String> para = (Vector<String>)parameter_pub.get(0);
				if (this.neighbourhood.get(sender.getAddress()) != null){
					this.serviceTable.addDevice(sender, para);
				}
				break;
			case UPDATE:
				Vector<Object> parameter_upd = (Vector<Object>) content.get(1);
				String oldServ = (String) parameter_upd.get(0);
				String newServ = (String) parameter_upd.get(1);
				if (this.neighbourhood.get(sender.getAddress()) != null){
					this.serviceTable.updateService(sender, oldServ, newServ);
				}
				break;
			}
		}
	}



	private void respGetServices(BluetoothDeviceAddress sender, Vector<String> result) {
		/**
		 * 0--> Message_Type 
		 * 1--> Vector<Object> ParameterList 
		 * 2--> in Response:
		 * Vector<Object> 3,..*--> further informations / ResultSets
		 */
		
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.RESP_GET_SERVICES);
		Vector<Object> parameterList = new Vector<Object>();
		parameterList.add(result);
		messageContent.add(parameterList);
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, sender);	
	}

	private void respGetServiceProvider(BluetoothDeviceAddress sender, String serviceName, Vector<BluetoothDeviceAddress> result) {
		/**
		 * 0--> Message_Type 
		 * 1--> Vector<Object> ParameterList 
		 * 2--> in Response:
		 * Vector<Object> 3,..*--> further informations / ResultSets
		 */
		
		//prepare for NetworkSend
		Vector<Object> messageContent = new Vector<Object>();
		messageContent.add(message_type.RESP_GET_SERVICEPROVIDER);
		Vector<Object> parameterList = new Vector<Object>();
		parameterList.add(serviceName);
		parameterList.add(result);
		messageContent.add(parameterList);
		Message msg = new Message (messageContent, true);
		//publish
		this.invokeSendMessage(msg, sender);	
	}

	private void processResultAskForService(BluetoothDeviceAddress sender, @SuppressWarnings("unused") Vector<String> vector) {
		System.out.println("FloodingPublishDiscovery: ask for Service at " +sender.getAddress() + " processed");
	}

	private void processResultFindService(BluetoothDeviceAddress sender, String service) {
		System.out.println("FloodingPublishDiscovery: find Service:" + service + "at: " +sender.getAddress() + " processed");		
	}

	///////////////////////////////////////////
	
	private void processResultGetAllServices(@SuppressWarnings("unused") BluetoothDeviceAddress sender, @SuppressWarnings("unused") Vector<String> vector) {
		System.out.println("FloodingPublishDiscovery: find all Services answer processed");				
	}

	private class ServiceTable {
	
		//Bluetoothdeviceadress as String --> BD_ADDR
		private HashMap <String, BluetoothDeviceAddress> getBD_ADD_For_String = new HashMap <String, BluetoothDeviceAddress>();
		//BluetoothdeviceAddress
		private HashMap <String, Vector<BluetoothDeviceAddress>> serviceToDevice = new HashMap <String, Vector<BluetoothDeviceAddress>>();
		//BluetoothdeviceAddress as String --> Services at Device
		private HashMap <String, Vector<String>> deviceToService = new HashMap <String, Vector<String>>();
		
		
		private HashMap <String , Boolean> devConcServ = new HashMap<String, Boolean>();
		
		public void addDevice(BluetoothDeviceAddress dev, Vector<String> services){
			if (getBD_ADD_For_String.get(dev.getAddress()) == null){
				deviceToService.put(dev.getAddress(), services);
				getBD_ADD_For_String.put(dev.getAddress(), dev);
				if (services != null){
					for (String serv: services){
						if (serviceToDevice.get(serv) == null){
							Vector<BluetoothDeviceAddress> provs = new Vector<BluetoothDeviceAddress>();
							provs.add(dev);
							serviceToDevice.put(serv, provs);
						} else {
							serviceToDevice.get(serv).add(dev);
						}
						devConcServ.put(dev.getAddress() + "|" + serv, true);
					}					
				}
			}
		}

		public void updateService(BluetoothDeviceAddress sender, String oldServ, String newServ) {
			if (getBD_ADD_For_String.get(sender.getAddress()) == null){
				this.getBD_ADD_For_String.put(sender.getAddress(), sender);
			}
			if (deviceToService.get(sender.getAddress()) != null){
				Vector<String> knownServices = deviceToService.get(sender.getAddress()); 
				knownServices.remove(oldServ);
				knownServices.add(newServ);
			} else {
				Vector<String> knownServices = new Vector<String> ();
				knownServices.add(newServ);
				deviceToService.put(sender.getAddress(), knownServices);
			}
			if (serviceToDevice.get(oldServ) != null){
				Vector<BluetoothDeviceAddress> devices = serviceToDevice.get(oldServ);
				for (int i = 0; i < devices.size(); i++){
					if (devices.get(i).isEqual(sender)){
						devices.remove(i);
					}
				}
			}
			if (serviceToDevice.get(newServ) != null){
				Vector<BluetoothDeviceAddress> devices = serviceToDevice.get(newServ);
				boolean found = false;
				for (int i = 0; i < devices.size(); i++){
					if (devices.get(i).isEqual(sender)){
						found=true;
					}
				}
				if (!found){
					devices.add(sender);
				}
			} else {
				Vector<BluetoothDeviceAddress> devices = new Vector<BluetoothDeviceAddress>();
				devices.add(sender);
				serviceToDevice.put(newServ, devices);
			}
			this.devConcServ.remove(sender.getAddress() + "|" + oldServ);
			this.devConcServ.put(sender.getAddress() + "|" + newServ, true);
		}

		public void removeServices(BluetoothDeviceAddress sender, Vector<String> services) {
			Vector<String> savedServices = this.deviceToService.get(sender.getAddress()); 
			for (String service : services){
				for (int i = 0; i < savedServices.size(); i++){
					if (service.equals(savedServices.get(i))){
						savedServices.remove(i);
					}
				}
			}
		}

		public Vector<String> getAllAvailableServices() {
			return new Vector<String>(this.serviceToDevice.keySet());
		}

		public Vector<BluetoothDeviceAddress> getHostsForService(String serviceDescription) {
			return this.serviceToDevice.get(serviceDescription);
		}

		public Vector<String> getServicesForHost(BluetoothDeviceAddress target) {
			return this.deviceToService.get(target.getAddress());
		}
		
		
	}

}